package wator.model;

import java.util.Iterator;
import java.util.TreeSet;
import java.util.logging.Logger;

import wator.util.GlobalConfiguration;


/**
 * This class represents the population in the wator universe. All agents are
 * stored as sorted list. Agent's position vectors are within the range defined
 * by the size vector.
 * 
 * @see #setSizeVector(IntegerVector)
 */
public class Population implements Iterable<Agent> {
	
	private int neighborRadius2 = Constants.DEFAULT_NEIGHBOR_RADIUS2;
	private int eatingRadius2 = Constants.DEFAULT_EATING_RADIUS2;

	private IntegerVector sizeVector, nullVector;
	private Topology topo;
	
	private int round;
	
	private final static Logger log = Logger.getLogger("Population");
	
	/* (non-Javadoc)
	 * This red-black tree stores the available agents and their distance to
	 * the origin.
	 */
	private TreeSet<Agent> agents;
	
	/**
	 * Sole constructor. Creates a empty population. Note that
	 * {@link #setSizeVector(IntegerVector)} should be called before all
	 * {@link #repopulate()} calls.
	 * 
	 * @param topology topology of the populations universe
	 * @see #setSizeVector(IntegerVector)
	 * @see #repopulate()
	 */
	public Population(Topology topology) {
		topo = topology;
		agents = new TreeSet<Agent>();
		round = 0;
	}
	
	/**
	 * Set a new size vector. If the vector has the same dimension as the old
	 * one, as much individuals as possible are preserved. If the dimension
	 * changes, the population is cleared.
	 * 
	 * @param sizeVector the new size vector 
	 */
	public void setSizeVector(IntegerVector sizeVector) {
		if (this.sizeVector == null || this.sizeVector.getDimension()
				!= sizeVector.getDimension()) {
			clear();
			this.sizeVector = sizeVector;
			nullVector = sizeVector.sub(sizeVector);
		} else {
			this.sizeVector = sizeVector;
			makeConsistent();
		}
	}
	
	/**
	 * Set the topology of the population's universe.
	 * 
	 * @param topology the new topology
	 */
	public void setTopology(Topology topology) {
		topo = topology;
		makeConsistent();
	}
	
	/**
	 * Delete each individual in the population.
	 */
	public void clear() {
		agents.clear();
	}
	
	/**
	 * Get all agents in a certain area. The area is a hyper-ball, defined by a
	 * position vector pointing to its center and the radius. This can be used 
	 * to determine the neighborhood of an agent in the population, since the
	 * agent at the position vector is not included in the list.
	 * 
	 * @param agent the agent in the middle of the hyper-ball
	 * @return the neighborhood around agents position vector
	 * @see Neighborhood
	 * @throws NullPointerException 
	 */
	public Neighborhood getNeighborhood	(Agent agent) {
		Neighborhood neighbors = new Neighborhood(topo,
				agent.getPositionVector(), neighborRadius2);
		final int maxDistance = agent.getPositionVector().length2() 
				+ neighborRadius2;
		final int minDistance = maxDistance - 2 * neighborRadius2;
		// Add neighbors with shorter position vectors
		for (Agent neighbor = agents.lower(agent); 
				neighbor != null 
					&& neighbor.getPositionVector().length2() >= minDistance;
				neighbor = agents.lower(neighbor)) {
			neighbors.addAgent(neighbor);
		}
		// Add neighbors with larger position vectors
		for (Agent neighbor = agents.higher(agent); 
				neighbor != null 
					&& neighbor.getPositionVector().length2() <= maxDistance;
				neighbor = agents.higher(neighbor)) {
			neighbors.addAgent(neighbor);
		}
		return neighbors;
	}
	
	/**
	 * Redistribute the population. At first, remove all individuals. Then, at 
	 * each cell in the universe, create an individual of one of the enabled
	 * species ({@link AgentConfiguration#isEnabled()} such that at the end 
	 * they sum up to the initial population size of the species.
	 * 
	 * @see AvailableAgents
	 * @see AgentConfiguration#isEnabled()
	 */
	synchronized public void repopulate() {
		clear();
		round = 0;
		int total = sizeVector.vol();
		for (AgentConfiguration config : 
				AvailableAgents.getInstance().getConfigurations()) {
			if (!config.isEnabled()) {
				continue;
			}
			Agent template = config.getTemplate();
			total -= config.getInitialPopulationSize();
			if (total <= 1) {
				if(GlobalConfiguration.debug)
					log.warning("No space left to add species.");
				break;
			}
			final int n = config.getInitialPopulationSize();
			for (int i = 0; i < n; i++) {
				Agent agent = (Agent) template.clone();
				do {
					agent.setPositionVector(
							sizeVector.generateRandomVectorInRange());
				} while (!addAgent(agent));
			}
		}
		if(GlobalConfiguration.debug)
			log.info("Population size is now " + agents.size());
	}
	
	/**
	 * Adds a single agent to the population. If the position is already
	 * occupied by another agent or the position is outside the feasible
	 * boundaries, do nothing and return <code>false</code>. Note that this
	 * method only includes intrinsic sanity checks (such at position check),
	 * not extrinsic conditions such as if the agent species is enabled in the
	 * <code>AvailableAgents</code> container or not.
	 * 
	 * @param agent the agent to add
	 * @return true if the agent was added successfully, false otherwise
	 * @throws NullPointerException if <code>agent</code> is null
	 */
	public boolean addAgent(Agent agent) {
		agent.setPositionVector(topo.distanceVector(agent.getPositionVector(),
				nullVector));
		if (agents.contains(agent)) {
			return false;
		}
		agents.add(agent);
		return true;
	}
	
	/* (non-Javadoc)
	 * Make the population consistent with an updated size vector or a new
	 * topology. Builds a new consistent red-black tree and changes the
	 * agents reference.
	 */
	private void makeConsistent() {
		nullVector = sizeVector.sub(sizeVector); 
		TreeSet<Agent> old = new TreeSet<Agent>(agents);
		for (Agent agent : old) {
			addAgent(agent);
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Iterator<Agent> iterator() {
		return agents.iterator();
	}
	
	/**
	 * Get the maximum distance power two for that two individuals still are
	 * considered to be adjacent.
	 * 
	 * @return the neighborhood radius power two
	 */
	public int getNeighborhoodRadius2() {
		return neighborRadius2;
	}

	/**
	 * Set the maximum distance power two for that two individuals still are
	 * considered to be adjacent. The new value is only used if it is larger
	 * than the current eating radius.
	 * 
	 * @param radius2 the new neighborhood radius power 2
	 * @see #getEatingRadius2()
	 */
	public void setNeighborhoodRadius2(int radius2) {
		if (radius2 >= eatingRadius2) {
			this.neighborRadius2 = radius2;
		}
	}

	/**
	 * Get the range in which predators can eat preys.
	 * 
	 * @return the maximum distance to a prey, in which a predator can eat
	 * 		directly 
	 */
	public int getEatingRadius2() {
		return eatingRadius2;
	}

	/**
	 * Set the range in which predators can eat preys. The new value is only 
	 * used if it is smaller than the current neighborhood radius.
	 * 
	 * @param radius2 the new eating radius power 2
	 * @see #getNeighborhoodRadius2()
	 */
	public void setEatingRadius2(int radius2) {
		if (radius2 >= neighborRadius2) {
			this.eatingRadius2 = radius2;
		}
	}

	/**
	 * Executes one iteration, in which individuals move, eat, breed, and die.
	 * Mediate decisions made by the individuals to neighbors and manages 
	 * synchronization with the population data set.
	 * 
	 * @see Simulation#iterate(int)
	 */
	synchronized public void iterate() {
		if (agents.isEmpty()) {
			return;
		}
		round++;
		Agent child;
		Agent[] toProcess = new Agent[]{};
		toProcess = agents.toArray(toProcess);
		for (Agent agent : toProcess) {
			agents.remove(agent);
			Neighborhood n = getNeighborhood(agent);
			if (agent.dies(n)) {
				continue; // Do not add again
			}
			for (Iterator<Neighborhood.Neighbor> it = n.iterator();
					it.hasNext();) {
				Neighborhood.Neighbor neighbor = it.next();
				if (neighbor.getDistanceVector().length2() <= eatingRadius2 
						&& agent.eats(neighbor)) {
					it.remove();
					agents.remove(neighbor.getAgent());
				}
			}
			IntegerVector d = agent.move(n);
			child = agent.breed(n);
			agent.setDirectionVector(d);
			agent.setPositionVector(topo.distanceVector(nullVector,
					agent.getPositionVector().add(d)));
			agents.add(agent); // Add updated agent
			if (child != null) {
				agents.add(child);
			}
		}
	}
	
	/**
	 * Get the number of individuals in the population of all species.
	 * 
	 * @return the number of individuals
	 */
	public int countIndividuals() {
		return agents.size();
	}
	
	/**
	 * Get the current round. It is reset when <code>repopulate()</code> is
	 * called, in increased when <code>iterate()</code> is called.
	 * 
	 * @return the current round
	 */
	public int getRound() {
		return round;
	}
}
